/**
 * Copyright 2018 jianggujin (www.jianggujin.com).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jianggujin.codec;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;

/**
 * Base64编码/解码
 * 
 * <p>
 * Basic
 * </p>
 * 使用RFC 4648和RFC 2045表1中规定的“Base64字母表”进行编码和解码操作。 编码器不添加任何换行符（行分隔符）字符。
 * 解码器拒绝包含base64字母外的字符的数据。
 * 
 * <p>
 * URL and Filename safe
 * </p>
 * 使用RFC 4648的表2中规定的“URL和Filename safe Base64 Alphabet”进行编码和解码。
 * 编码器不添加任何换行符（行分隔符）字符。 解码器拒绝包含base64字母外的字符的数据。
 * 
 * <p>
 * MIME
 * </p>
 * 使用RFC 2045表1中规定的“Base64字母表”进行编码和解码操作。 编码输出必须以不超过76个字符的行'\n'
 * ，并使用回车'\r'然后立即以换行'\n'作为行分隔符。 没有行分隔符添加到编码输出的末尾。
 * 在解码操作中，将忽略base64字母表中未找到的所有行分隔符或其他字符。
 * 
 * 
 * @author jianggujin
 *
 */
public final class JBase64 {
   private JBase64() {
   }

   /**
    * 返回一个{@link JEncoder}，使用Basic型base64编码方案
    * 
    * @return
    */
   public static JEncoder getEncoder() {
      return JEncoder.RFC4648;
   }

   /**
    * 返回一个{@link JEncoder}，使用URL and Filename safe型base64编码方案
    * 
    * @return
    */
   public static JEncoder getUrlEncoder() {
      return JEncoder.RFC4648_URLSAFE;
   }

   /**
    * 返回一个{@link JEncoder}，使用MIME型base64编码方案
    * 
    * @return
    */
   public static JEncoder getMimeEncoder() {
      return JEncoder.RFC2045;
   }

   /**
    * 返回一个{@link JEncoder}，它使用具有指定行长和行分隔符的MIME类型base64编码方案进行编码
    * 
    * @param lineLength
    *           每个输出行的长度（向下舍入为4的最接近的倍数）。如果lineLength &lt;= 0的输出不会被分开
    * @param lineSeparator
    *           每个输出行的行分隔符
    * @return
    */
   public static JEncoder getMimeEncoder(int lineLength, byte[] lineSeparator) {
      if (lineSeparator == null)
         throw new NullPointerException();
      int[] base64 = JDecoder.fromBase64;
      for (byte b : lineSeparator) {
         if (base64[b & 0xff] != -1)
            throw new IllegalArgumentException("Illegal base64 line separator character 0x" + Integer.toString(b, 16));
      }
      if (lineLength <= 0) {
         return JEncoder.RFC4648;
      }
      return new JEncoder(false, lineSeparator, lineLength >> 2 << 2, true);
   }

   /**
    * 返回一个{@link JDecoder}，使用Basic型base64解码方案
    * 
    * @return
    */
   public static JDecoder getDecoder() {
      return JDecoder.RFC4648;
   }

   /**
    * 返回一个{@link JDecoder}，使用URL and Filename safe型base64解码方案
    * 
    * @return
    */
   public static JDecoder getUrlDecoder() {
      return JDecoder.RFC4648_URLSAFE;
   }

   /**
    * 返回一个{@link JDecoder}，使用MIME型base64解码方案
    * 
    * @return
    */
   public static JDecoder getMimeDecoder() {
      return JDecoder.RFC2045;
   }

   /**
    * 该类使用RFC 4648和RFC 2045中规定的Base64编码方案来实现用于编码字节数据的编码器，线程安全
    * 
    * @author jianggujin
    *
    */
   public static class JEncoder {

      private final byte[] newline;
      private final int linemax;
      private final boolean isURL;
      private final boolean doPadding;

      private JEncoder(boolean isURL, byte[] newline, int linemax, boolean doPadding) {
         this.isURL = isURL;
         this.newline = newline;
         this.linemax = linemax;
         this.doPadding = doPadding;
      }

      /**
       * This array is a lookup table that translates 6-bit positive integer
       * index values into their "Base64 Alphabet" equivalents as specified in
       * "Table 1: The Base64 Alphabet" of RFC 2045 (and RFC 4648).
       */
      private static final char[] toBase64 = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
            'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
            'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '+', '/' };

      /**
       * It's the lookup table for "URL and Filename safe Base64" as specified
       * in Table 2 of the RFC 4648, with the '+' and '/' changed to '-' and
       * '_'. This table is used when BASE64_URL is specified.
       */
      private static final char[] toBase64URL = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
            'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
            'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '-', '_' };

      private static final int MIMELINEMAX = 76;
      private static final byte[] CRLF = new byte[] { '\r', '\n' };

      static final JEncoder RFC4648 = new JEncoder(false, null, -1, true);
      static final JEncoder RFC4648_URLSAFE = new JEncoder(true, null, -1, true);
      static final JEncoder RFC2045 = new JEncoder(false, CRLF, MIMELINEMAX, true);

      private final int outLength(int srclen) {
         int len = 0;
         if (doPadding) {
            len = 4 * ((srclen + 2) / 3);
         } else {
            int n = srclen % 3;
            len = 4 * (srclen / 3) + (n == 0 ? 0 : n + 1);
         }
         if (linemax > 0) // line separators
            len += (len - 1) / linemax * newline.length;
         return len;
      }

      /**
       * 使用Base64编码方案将指定字节数组中的所有字节编码为新分配的字节数组。 返回的字节数组是生成字节的长度。
       * 
       * @param src
       * @return
       */
      public byte[] encode(byte[] src) {
         int len = outLength(src.length); // dst array size
         byte[] dst = new byte[len];
         int ret = encode0(src, 0, src.length, dst);
         if (ret != dst.length)
            return Arrays.copyOf(dst, ret);
         return dst;
      }

      /**
       * 使用Base64编码方案对指定字节数组中的所有字节进行编码，将结果字节写入给定的输出字节数组，从偏移量0开始。
       * 这种方法的调用者有责任确保输出字节数组dst具有足够的空间来编码来自输入字节数组的所有字节。
       * 如果输出字节数组不够大，则不会将字节写入输出字节数组。
       * 
       * @param src
       * @param dst
       * @return
       */
      public int encode(byte[] src, byte[] dst) {
         int len = outLength(src.length); // dst array size
         if (dst.length < len)
            throw new IllegalArgumentException("Output byte array is too small for encoding all input bytes");
         return encode0(src, 0, src.length, dst);
      }

      /**
       * 使用Base64编码方案将指定的字节数组编码为String。
       * 
       * @param src
       * @return
       */
      public String encodeToString(byte[] src, String charset) {
         byte[] encoded = encode(src);
         return new String(encoded, Charset.forName(charset));
         // new String(encoded, 0, 0, encoded.length);
      }

      /**
       * 使用Base64编码方案将所有剩余字节从指定的字节缓冲区编码到新分配的ByteBuffer中。 返回时，源缓冲区的位置将更新为其限制;
       * 其限制将不会改变。 返回的输出缓冲区的位置将为零，其限制将是生成的编码字节数。
       * 
       * @param buffer
       * @return
       */
      public ByteBuffer encode(ByteBuffer buffer) {
         int len = outLength(buffer.remaining());
         byte[] dst = new byte[len];
         int ret = 0;
         if (buffer.hasArray()) {
            ret = encode0(buffer.array(), buffer.arrayOffset() + buffer.position(),
                  buffer.arrayOffset() + buffer.limit(), dst);
            buffer.position(buffer.limit());
         } else {
            byte[] src = new byte[buffer.remaining()];
            buffer.get(src);
            ret = encode0(src, 0, src.length, dst);
         }
         if (ret != dst.length)
            dst = Arrays.copyOf(dst, ret);
         return ByteBuffer.wrap(dst);
      }

      /**
       * 使用Base64编码方案包装用于编码字节数据的输出流。 建议在使用后立即关闭返回的输出流，在此期间，它会将所有可能的剩余字节刷新到底层输出流。
       * 关闭返回的输出流将关闭底层输出流。
       * 
       * @param os
       * @return
       */
      public OutputStream wrap(OutputStream os) {
         if (os == null)
            throw new NullPointerException();
         return new JEncOutputStream(os, isURL ? toBase64URL : toBase64, newline, linemax, doPadding);
      }

      /**
       * 返回一个编码器实例，编码器等效于此编码器实例，但不会在编码字节数据的末尾添加任何填充字符。 该编码器实例的编码方案不受此调用的影响。
       * 返回的编码器实例应用于非填充编码操作。
       * 
       * @return
       */
      public JEncoder withoutPadding() {
         if (!doPadding)
            return this;
         return new JEncoder(isURL, newline, linemax, false);
      }

      private int encode0(byte[] src, int off, int end, byte[] dst) {
         char[] base64 = isURL ? toBase64URL : toBase64;
         int sp = off;
         int slen = (end - off) / 3 * 3;
         int sl = off + slen;
         if (linemax > 0 && slen > linemax / 4 * 3)
            slen = linemax / 4 * 3;
         int dp = 0;
         while (sp < sl) {
            int sl0 = Math.min(sp + slen, sl);
            for (int sp0 = sp, dp0 = dp; sp0 < sl0;) {
               int bits = (src[sp0++] & 0xff) << 16 | (src[sp0++] & 0xff) << 8 | (src[sp0++] & 0xff);
               dst[dp0++] = (byte) base64[(bits >>> 18) & 0x3f];
               dst[dp0++] = (byte) base64[(bits >>> 12) & 0x3f];
               dst[dp0++] = (byte) base64[(bits >>> 6) & 0x3f];
               dst[dp0++] = (byte) base64[bits & 0x3f];
            }
            int dlen = (sl0 - sp) / 3 * 4;
            dp += dlen;
            sp = sl0;
            if (dlen == linemax && sp < end) {
               for (byte b : newline) {
                  dst[dp++] = b;
               }
            }
         }
         if (sp < end) { // 1 or 2 leftover bytes
            int b0 = src[sp++] & 0xff;
            dst[dp++] = (byte) base64[b0 >> 2];
            if (sp == end) {
               dst[dp++] = (byte) base64[(b0 << 4) & 0x3f];
               if (doPadding) {
                  dst[dp++] = '=';
                  dst[dp++] = '=';
               }
            } else {
               int b1 = src[sp++] & 0xff;
               dst[dp++] = (byte) base64[(b0 << 4) & 0x3f | (b1 >> 4)];
               dst[dp++] = (byte) base64[(b1 << 2) & 0x3f];
               if (doPadding) {
                  dst[dp++] = '=';
               }
            }
         }
         return dp;
      }
   }

   /**
    * 该类使用RFC 4648和RFC 2045中规定的Base64编码方案来实现用于解码字节数据的解码器。
    * Base64填充字符'='被接受并被解释为编码字节数据的结尾，但不是必需的。
    * 因此，如果编码字节数据的最终单位只有两个或三个Base64字符（没有相应的填充字符被填充），则它们被解码，就像后跟填充字符一样。
    * 如果最终单位中存在填充字符，则必须存在正确的填充字符数，否则在解码期间抛出IllegalArgumentException
    * （从Base64流读取时为IOException ）。
    * 
    * @author jianggujin
    *
    */
   public static class JDecoder {

      private final boolean isURL;
      private final boolean isMIME;

      private JDecoder(boolean isURL, boolean isMIME) {
         this.isURL = isURL;
         this.isMIME = isMIME;
      }

      /**
       * Lookup table for decoding unicode characters drawn from the "Base64
       * Alphabet" (as specified in Table 1 of RFC 2045) into their 6-bit
       * positive integer equivalents. Characters that are not in the Base64
       * alphabet but fall within the bounds of the array are encoded to -1.
       *
       */
      private static final int[] fromBase64 = new int[256];
      static {
         Arrays.fill(fromBase64, -1);
         for (int i = 0; i < JEncoder.toBase64.length; i++)
            fromBase64[JEncoder.toBase64[i]] = i;
         fromBase64['='] = -2;
      }

      /**
       * Lookup table for decoding "URL and Filename safe Base64 Alphabet" as
       * specified in Table2 of the RFC 4648.
       */
      private static final int[] fromBase64URL = new int[256];

      static {
         Arrays.fill(fromBase64URL, -1);
         for (int i = 0; i < JEncoder.toBase64URL.length; i++)
            fromBase64URL[JEncoder.toBase64URL[i]] = i;
         fromBase64URL['='] = -2;
      }

      static final JDecoder RFC4648 = new JDecoder(false, false);
      static final JDecoder RFC4648_URLSAFE = new JDecoder(true, false);
      static final JDecoder RFC2045 = new JDecoder(false, true);

      /**
       * 使用Base64编码方案从输入字节数组中解码所有字节，将结果写入新分配的输出字节数组。 返回的字节数组是生成字节的长度。
       * 
       * @param src
       * @return
       */
      public byte[] decode(byte[] src) {
         byte[] dst = new byte[outLength(src, 0, src.length)];
         int ret = decode0(src, 0, src.length, dst);
         if (ret != dst.length) {
            dst = Arrays.copyOf(dst, ret);
         }
         return dst;
      }

      /**
       * 使用Base64编码方案将Base64编码的字符串解码为新分配的字节数组。
       * @param src
       * @param charset
       * @return
       */
      public byte[] decode(String src, String charset) {
         return decode(src.getBytes(Charset.forName(charset)));
      }

      /**
       * 使用Base64编码方案从输入字节数组中解码所有字节，将结果写入给定的输出字节数组，从偏移0开始。
       * 这种方法的调用者有责任确保输出字节数组dst具有足够的空间来解码输入字节数组中的所有字节。
       * 如果输出字节数组不够大，则不会将字节写入输出字节数组。
       * 
       * 如果输入字节数组不是有效的Base64编码方案，那么在抛出IllegalargumentException之前，
       * 可能会将一些字节写入输出字节数组。
       * 
       * @param src
       * @param dst
       * @return
       */
      public int decode(byte[] src, byte[] dst) {
         int len = outLength(src, 0, src.length);
         if (dst.length < len)
            throw new IllegalArgumentException("Output byte array is too small for decoding all input bytes");
         return decode0(src, 0, src.length, dst);
      }

      /**
       * 使用Base64编码方案从输入字节缓冲区中解码所有字节，将结果写入新分配的ByteBuffer。 返回时，源缓冲区的位置将更新为其限制;
       * 其限制将不会改变。 返回的输出缓冲区的位置将为零，其限制将是生成的解码字节数
       * 
       * 如果输入缓冲区不在有效的Base64编码方案中，则抛出IllegalArgumentException 。
       * 在这种情况下，输入缓冲区的位置不会提前。
       * 
       * @param buffer
       * @return
       */
      public ByteBuffer decode(ByteBuffer buffer) {
         int pos0 = buffer.position();
         try {
            byte[] src;
            int sp, sl;
            if (buffer.hasArray()) {
               src = buffer.array();
               sp = buffer.arrayOffset() + buffer.position();
               sl = buffer.arrayOffset() + buffer.limit();
               buffer.position(buffer.limit());
            } else {
               src = new byte[buffer.remaining()];
               buffer.get(src);
               sp = 0;
               sl = src.length;
            }
            byte[] dst = new byte[outLength(src, sp, sl)];
            return ByteBuffer.wrap(dst, 0, decode0(src, sp, sl, dst));
         } catch (IllegalArgumentException iae) {
            buffer.position(pos0);
            throw iae;
         }
      }

      /**
       * 返回一个输入流，用于解码Base64编码字节流。
       * 返回的InputStream的read方法在读取不能解码的字节时将抛出IOException。
       * 
       * 关闭返回的输入流将关闭底层输入流。
       * 
       * @param is
       * @return
       */
      public InputStream wrap(InputStream is) {
         if (is == null)
            throw new NullPointerException();
         return new JDecInputStream(is, isURL ? fromBase64URL : fromBase64, isMIME);
      }

      private int outLength(byte[] src, int sp, int sl) {
         int[] base64 = isURL ? fromBase64URL : fromBase64;
         int paddings = 0;
         int len = sl - sp;
         if (len == 0)
            return 0;
         if (len < 2) {
            if (isMIME && base64[0] == -1)
               return 0;
            throw new IllegalArgumentException("Input byte[] should at least have 2 bytes for base64 bytes");
         }
         if (isMIME) {
            // scan all bytes to fill out all non-alphabet. a performance
            // trade-off of pre-scan or Arrays.copyOf
            int n = 0;
            while (sp < sl) {
               int b = src[sp++] & 0xff;
               if (b == '=') {
                  len -= (sl - sp + 1);
                  break;
               }
               if ((b = base64[b]) == -1)
                  n++;
            }
            len -= n;
         } else {
            if (src[sl - 1] == '=') {
               paddings++;
               if (src[sl - 2] == '=')
                  paddings++;
            }
         }
         if (paddings == 0 && (len & 0x3) != 0)
            paddings = 4 - (len & 0x3);
         return 3 * ((len + 3) / 4) - paddings;
      }

      private int decode0(byte[] src, int sp, int sl, byte[] dst) {
         int[] base64 = isURL ? fromBase64URL : fromBase64;
         int dp = 0;
         int bits = 0;
         int shiftto = 18; // pos of first byte of 4-byte atom
         while (sp < sl) {
            int b = src[sp++] & 0xff;
            if ((b = base64[b]) < 0) {
               if (b == -2) { // padding byte '='
                              // = shiftto==18 unnecessary padding
                              // x= shiftto==12 a dangling single x
                              // x to be handled together with non-padding case
                              // xx= shiftto==6&&sp==sl missing last =
                              // xx=y shiftto==6 last is not =
                  if (shiftto == 6 && (sp == sl || src[sp++] != '=') || shiftto == 18) {
                     throw new IllegalArgumentException("Input byte array has wrong 4-byte ending unit");
                  }
                  break;
               }
               if (isMIME) // skip if for rfc2045
                  continue;
               else throw new IllegalArgumentException("Illegal base64 character " + Integer.toString(src[sp - 1], 16));
            }
            bits |= (b << shiftto);
            shiftto -= 6;
            if (shiftto < 0) {
               dst[dp++] = (byte) (bits >> 16);
               dst[dp++] = (byte) (bits >> 8);
               dst[dp++] = (byte) (bits);
               shiftto = 18;
               bits = 0;
            }
         }
         // reached end of byte array or hit padding '=' characters.
         if (shiftto == 6) {
            dst[dp++] = (byte) (bits >> 16);
         } else if (shiftto == 0) {
            dst[dp++] = (byte) (bits >> 16);
            dst[dp++] = (byte) (bits >> 8);
         } else if (shiftto == 12) {
            // dangling single "x", incorrectly encoded.
            throw new IllegalArgumentException("Last unit does not have enough valid bits");
         }
         // anything left is invalid, if is not MIME.
         // if MIME, ignore all non-base64 character
         while (sp < sl) {
            if (isMIME && base64[src[sp++]] < 0)
               continue;
            throw new IllegalArgumentException("Input byte array has incorrect ending byte at " + sp);
         }
         return dp;
      }
   }

   /*
    * An output stream for encoding bytes into the Base64.
    */
   private static class JEncOutputStream extends FilterOutputStream {

      private int leftover = 0;
      private int b0, b1, b2;
      private boolean closed = false;

      private final char[] base64; // byte->base64 mapping
      private final byte[] newline; // line separator, if needed
      private final int linemax;
      private final boolean doPadding;// whether or not to pad
      private int linepos = 0;

      JEncOutputStream(OutputStream os, char[] base64, byte[] newline, int linemax, boolean doPadding) {
         super(os);
         this.base64 = base64;
         this.newline = newline;
         this.linemax = linemax;
         this.doPadding = doPadding;
      }

      @Override
      public void write(int b) throws IOException {
         byte[] buf = new byte[1];
         buf[0] = (byte) (b & 0xff);
         write(buf, 0, 1);
      }

      private void checkNewline() throws IOException {
         if (linepos == linemax) {
            out.write(newline);
            linepos = 0;
         }
      }

      @Override
      public void write(byte[] b, int off, int len) throws IOException {
         if (closed)
            throw new IOException("Stream is closed");
         if (off < 0 || len < 0 || off + len > b.length)
            throw new ArrayIndexOutOfBoundsException();
         if (len == 0)
            return;
         if (leftover != 0) {
            if (leftover == 1) {
               b1 = b[off++] & 0xff;
               len--;
               if (len == 0) {
                  leftover++;
                  return;
               }
            }
            b2 = b[off++] & 0xff;
            len--;
            checkNewline();
            out.write(base64[b0 >> 2]);
            out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]);
            out.write(base64[(b1 << 2) & 0x3f | (b2 >> 6)]);
            out.write(base64[b2 & 0x3f]);
            linepos += 4;
         }
         int nBits24 = len / 3;
         leftover = len - (nBits24 * 3);
         while (nBits24-- > 0) {
            checkNewline();
            int bits = (b[off++] & 0xff) << 16 | (b[off++] & 0xff) << 8 | (b[off++] & 0xff);
            out.write(base64[(bits >>> 18) & 0x3f]);
            out.write(base64[(bits >>> 12) & 0x3f]);
            out.write(base64[(bits >>> 6) & 0x3f]);
            out.write(base64[bits & 0x3f]);
            linepos += 4;
         }
         if (leftover == 1) {
            b0 = b[off++] & 0xff;
         } else if (leftover == 2) {
            b0 = b[off++] & 0xff;
            b1 = b[off++] & 0xff;
         }
      }

      @Override
      public void close() throws IOException {
         if (!closed) {
            closed = true;
            if (leftover == 1) {
               checkNewline();
               out.write(base64[b0 >> 2]);
               out.write(base64[(b0 << 4) & 0x3f]);
               if (doPadding) {
                  out.write('=');
                  out.write('=');
               }
            } else if (leftover == 2) {
               checkNewline();
               out.write(base64[b0 >> 2]);
               out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]);
               out.write(base64[(b1 << 2) & 0x3f]);
               if (doPadding) {
                  out.write('=');
               }
            }
            leftover = 0;
            out.close();
         }
      }
   }

   /*
    * An input stream for decoding Base64 bytes
    */
   private static class JDecInputStream extends InputStream {

      private final InputStream is;
      private final boolean isMIME;
      private final int[] base64; // base64 -> byte mapping
      private int bits = 0; // 24-bit buffer for decoding
      private int nextin = 18; // next available "off" in "bits" for input;
                               // -> 18, 12, 6, 0
      private int nextout = -8; // next available "off" in "bits" for output;
                                // -> 8, 0, -8 (no byte for output)
      private boolean eof = false;
      private boolean closed = false;

      JDecInputStream(InputStream is, int[] base64, boolean isMIME) {
         this.is = is;
         this.base64 = base64;
         this.isMIME = isMIME;
      }

      private byte[] sbBuf = new byte[1];

      @Override
      public int read() throws IOException {
         return read(sbBuf, 0, 1) == -1 ? -1 : sbBuf[0] & 0xff;
      }

      @Override
      public int read(byte[] b, int off, int len) throws IOException {
         if (closed)
            throw new IOException("Stream is closed");
         if (eof && nextout < 0) // eof and no leftover
            return -1;
         if (off < 0 || len < 0 || len > b.length - off)
            throw new IndexOutOfBoundsException();
         int oldOff = off;
         if (nextout >= 0) { // leftover output byte(s) in bits buf
            do {
               if (len == 0)
                  return off - oldOff;
               b[off++] = (byte) (bits >> nextout);
               len--;
               nextout -= 8;
            } while (nextout >= 0);
            bits = 0;
         }
         while (len > 0) {
            int v = is.read();
            if (v == -1) {
               eof = true;
               if (nextin != 18) {
                  if (nextin == 12)
                     throw new IOException("Base64 stream has one un-decoded dangling byte.");
                  // treat ending xx/xxx without padding character legal.
                  // same logic as v == '=' below
                  b[off++] = (byte) (bits >> (16));
                  len--;
                  if (nextin == 0) { // only one padding byte
                     if (len == 0) { // no enough output space
                        bits >>= 8; // shift to lowest byte
                        nextout = 0;
                     } else {
                        b[off++] = (byte) (bits >> 8);
                     }
                  }
               }
               if (off == oldOff)
                  return -1;
               else return off - oldOff;
            }
            if (v == '=') { // padding byte(s)
                            // = shiftto==18 unnecessary padding
                            // x= shiftto==12 dangling x, invalid unit
                            // xx= shiftto==6 && missing last '='
                            // xx=y or last is not '='
               if (nextin == 18 || nextin == 12 || nextin == 6 && is.read() != '=') {
                  throw new IOException("Illegal base64 ending sequence:" + nextin);
               }
               b[off++] = (byte) (bits >> (16));
               len--;
               if (nextin == 0) { // only one padding byte
                  if (len == 0) { // no enough output space
                     bits >>= 8; // shift to lowest byte
                     nextout = 0;
                  } else {
                     b[off++] = (byte) (bits >> 8);
                  }
               }
               eof = true;
               break;
            }
            if ((v = base64[v]) == -1) {
               if (isMIME) // skip if for rfc2045
                  continue;
               else throw new IOException("Illegal base64 character " + Integer.toString(v, 16));
            }
            bits |= (v << nextin);
            if (nextin == 0) {
               nextin = 18; // clear for next
               nextout = 16;
               while (nextout >= 0) {
                  b[off++] = (byte) (bits >> nextout);
                  len--;
                  nextout -= 8;
                  if (len == 0 && nextout >= 0) { // don't clean "bits"
                     return off - oldOff;
                  }
               }
               bits = 0;
            } else {
               nextin -= 6;
            }
         }
         return off - oldOff;
      }

      @Override
      public int available() throws IOException {
         if (closed)
            throw new IOException("Stream is closed");
         return is.available(); // TBD:
      }

      @Override
      public void close() throws IOException {
         if (!closed) {
            closed = true;
            is.close();
         }
      }
   }
}
